package pl.enigmatic.math.interpolation;

public abstract class Interpolator {

	protected double first;
	protected double last;
	protected double[] frames;
	protected double[][] points;
	protected double[] lastValue;
	protected int dimension = 0;

	/**
	 * @param frames frame values - the 'x' axis
	 * @param points <code>point[i]</code> represents multidimensional value at frame <code>frames[i]</code>
	 */
	public Interpolator(final double frames[], final double points[][]) {
		initialize(frames, points);
	}

	/**
	 * the same as calling constructor {@link #Interpolator(double[], double[][])} with parameters <code>coord[0]</code> and transposed
	 * <code>coord[1 .. coord.length-1]</code>
	 * 
	 * @param coord <code>coord[0]</code> must be frames, the rest (from 1 to coord.length-1) are values of coordinates on axes one by one. The
	 *        following term must be satisfied: <code>coord[i].length == coord[j].length</code> for all <code>i, j</code>.
	 */
	public Interpolator(final double[]... coord) {
		final double[][] points = new double[coord[0].length][coord.length - 1];
		for (int i = 0; i < coord[0].length; i++) {
			points[i] = new double[coord.length - 1];
			for (int j = 1; j < coord.length; j++) {
				points[i][j - 1] = coord[j][i];
			}
		}
		initialize(coord[0], points);
	}

	private void initialize(final double frames[], final double points[][]) {
		if (frames != null && points != null) {
			this.points = points;
			dimension = points[0].length;
			first = frames[0];
			last = frames[frames.length - 1];
			this.frames = new double[frames.length];
			for (int i = 0; i < this.frames.length; i++) {
				this.frames[i] = (frames[i] - first) / (last - first);
			}
			lastValue = new double[this.points[0].length];
			for (int i = 0; i < this.points[0].length; i++) {
				lastValue[i] = this.points[0][i];
			}
		}
	}

	protected abstract double[] getValue(double alpha, int rng, double t);

	public double[] getValue(final double alpha) {
		if (alpha == 0) {
			lastValue = points[0].clone();
		} else if (alpha == 1) {
			lastValue = points[frames.length - 1].clone();
		} else if (alpha > 0 && alpha < 1) {
			int i = 0;
			while (frames[i + 1] < alpha) {
				i++;
			}
			final double t = (alpha - frames[i]) / (frames[i + 1] - frames[i]);
			lastValue = getValue(alpha, i, t);
		} else {
			lastValue = zero(dimension);
		}
		return lastValue;
	}

	public double[] getValueAtFrame(final double frame) {
		return getValue((frame - first) / (last - first));
	}

	public double[] getDValue(final double alpha) {
		final double[] value = lastValue;
		lastValue = getValue(alpha);
		for (int i = 0; i < value.length; i++) {
			value[i] = lastValue[i] - value[i];
		}
		return value;
	};

	public double[] getDMulValue(final double alpha) {
		final double[] value = lastValue;
		lastValue = getValue(alpha);
		for (int i = 0; i < value.length; i++) {
			value[i] = lastValue[i] / value[i];
		}
		return value;
	};

	public double[] getDValueAtFrame(final double frame) {
		return getDValue((frame - first) / (last - first));
	}

	public double[] getDMulValueAtFrame(final double frame) {
		return getDMulValue((frame - first) / (last - first));
	}

	private static double[] zero(final int d) {
		final double res[] = new double[d];
		for (int i = 0; i < d; i++) {
			res[i] = 0;
		}
		return res;
	}

	public double getFirstFrame() {
		return first;
	}

	public double getLastFrame() {
		return last;
	}

	public abstract static class Single {

		private Interpolator interpolator;

		public double frame(final int i) {
			return interpolator.frames[i];
		}

		public double value(final int i) {
			return interpolator.points[i][0];
		}

		public Single(final double[] frames, final double[] points) {
			final double[][] res = new double[points.length][];
			for (int i = 0; i < points.length; i++) {
				res[i] = new double[] { points[i] };
			}
			interpolator = new Interpolator(frames, res) {

				@Override
				protected double[] getValue(final double alpha, final int rng, final double t) {
					return new double[] { Single.this.getValue(alpha, rng, t) };
				}
			};
		}

		public double getValueAtFrame(final double frame) {
			return interpolator.getValueAtFrame(frame)[0];
		}

		protected abstract double getValue(double alpha, int rng, double t);

		public double getValue(final double alpha) {
			return interpolator.getValue(alpha)[0];
		}

		public double getDValue(final double alpha) {
			return interpolator.getDValue(alpha)[0];
		}

		public double getDMulValue(final double alpha) {
			return interpolator.getDMulValue(alpha)[0];
		}

		public double getFirstFrame() {
			return interpolator.getFirstFrame();
		}

		public double getLastFrame() {
			return interpolator.getLastFrame();
		}
	}
}
